package cc.aileron.accessor.properties;

import static cc.aileron.accessor.PojoAccessor.Type.GET;
import static cc.aileron.accessor.PojoAccessor.Type.SET;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

import cc.aileron.accessor.PojoAccessor;
import cc.aileron.accessor.PojoAccessorNotFoundError;
import cc.aileron.accessor.PojoAccessorSetError;
import cc.aileron.dxo.NumberDxo;
import cc.aileron.dxo.StringToNumberDxo;

/**
 * @author Aileron
 */
interface Accessor extends AccessorGetter, AccessorSetter
{
}

/**
 * @author Aileron
 */
interface AccessorGetter
{
    /**
     * @param target
     * @param key
     * @return value
     */
    Object get(Object target,
            String key);

    /**
     * @return type
     */
    Class<?> resultType();
}

/**
 * @author Aileron
 */
interface AccessorMap
{
    /**
     * @param target
     * @param key
     * @param value
     */
    void set(Object target,
            String key,
            Object value);
}

/**
 * @author Aileron
 */
interface AccessorSetter
{
    /**
     * @return type
     */
    Class<?> argumentType();

    /**
     * @param target
     * @param value
     */
    void set(Object target,
            Object value);
}

/**
 * PojoProperties 実態
 * 
 * @author Aileron
 * 
 * @param <T>
 */
class PojoPropertiesImpl<T> implements PojoProperties<T>
{
    private static final String[] getterPatterns = { "get", "is", "to", "" };
    private static final HashMap<String, Void> mapAccessorKeys = new HashMap<String, Void>();
    private static final String[] mapAccessorPatterns = { "set", "put" };
    private static final NumberConvertor numberConvertor;
    private static final String[] setterPatterns = { "set", "" };
    static
    {
        for (final String key : new String[] { "get", "set", "put" })
        {
            mapAccessorKeys.put(key, null);
        }
        final NumberDxo numberDxo = new NumberDxo();
        final StringToNumberDxo stringDxo = new StringToNumberDxo(numberDxo);
        numberConvertor = new NumberConvertorImpl(numberDxo, stringDxo);
    }

    public Object get(final T target,
            final String name)
    {
        /*
         * self
         */
        if (name.equals("self"))
        {
            return target;
        }

        /*
         * getter
         */
        final String key = name.toLowerCase();
        for (final String pattern : getterPatterns)
        {
            final String methodName = pattern + key;
            final AccessorGetter accessor = getter.get(methodName);
            if (accessor != null)
            {
                return accessor.get(target, methodName);
            }
        }

        /*
         * field access
         */
        final AccessorGetter accessor = fields.get(key);
        if (accessor != null)
        {
            return accessor.get(target, key);
        }

        /*
         * get mapAccessor
         */
        final Method method = mapAccessor.get("get");
        if (method != null)
        {
            try
            {
                return method.invoke(target, key);
            }
            catch (final Exception e)
            {
                throw new Error(e);
            }
        }

        throw new PojoAccessorNotFoundError(this.targetClass, name, GET);
    }

    public void set(final T target,
            final String name,
            final Object value)
    {
        /*
         * setter
         */
        final String key = name.toLowerCase();
        for (final String pattern : setterPatterns)
        {
            final String methodName = pattern + key;
            final AccessorSetter accessor = setter.get(methodName);
            if (accessor != null)
            {
                final Class<?> type = accessor.argumentType();
                final Object val = convertValue(type, value);
                accessor.set(target, val);
                return;
            }
        }

        /*
         * field access
         */
        {
            final AccessorSetter accessor = fields.get(key);
            if (accessor != null)
            {
                final Class<?> type = accessor.argumentType();
                final Object val = convertValue(type, value);
                accessor.set(target, val);
                return;
            }
        }

        /*
         * set mapAccessor
         */
        for (final String methodName : mapAccessorPatterns)
        {
            final Method method = mapAccessor.get(methodName);
            if (method != null)
            {
                try
                {
                    final Class<?> type = method.getParameterTypes()[1];
                    final Object val = convertValue(type, value);
                    method.invoke(target, key, val);
                    return;
                }
                catch (final Exception e)
                {
                    throw new Error(e);
                }
            }
        }

        throw new PojoAccessorNotFoundError(targetClass, name, SET);
    }

    public Class<?> type(final PojoAccessor.Type type,
            final String name)
    {
        switch (type)
        {
        case GET:
            return typeGetter(name);
        case SET:
            return typeSetter(name);
        default:
            return null;
        }
    }

    /**
     * execute setter
     * 
     * @param accessor
     * @param target
     * @param value
     */
    private Object convertValue(final Class<?> type,
            final Object value)
    {
        return numberConvertor.convertValue(type, value);
    }

    /**
     * @param name
     * @return type
     */
    private Class<?> typeGetter(final String name)
    {
        final String key = name.toLowerCase();
        for (final String pattern : getterPatterns)
        {
            final AccessorGetter accessor = getter.get(pattern + key);
            if (accessor != null)
            {
                return accessor.resultType();
            }
        }
        final AccessorGetter accessor = fields.get(key);
        if (accessor != null)
        {
            return accessor.resultType();
        }
        throw new PojoAccessorNotFoundError(targetClass, name, GET);
    }

    /**
     * @param name
     * @return type
     */
    private Class<?> typeSetter(final String name)
    {
        final String key = name.toLowerCase();
        for (final String pattern : setterPatterns)
        {
            final AccessorSetter accessor = setter.get(pattern + key);
            if (accessor != null)
            {
                return accessor.argumentType();
            }
        }
        final AccessorSetter accessor = fields.get(key);
        if (accessor != null)
        {
            return accessor.argumentType();
        }
        throw new PojoAccessorNotFoundError(targetClass, name, SET);
    }

    /**
     * constractor
     * 
     * @param targetClass
     */
    public PojoPropertiesImpl(final Class<T> targetClass)
    {
        this.targetClass = targetClass;
        for (final Method method : targetClass.getMethods())
        {
            if (!method.isAccessible())
            {
                try
                {
                    method.setAccessible(true);
                }
                catch (final SecurityException e)
                {
                    continue;
                }
            }
            final String methodName = method.getName()
                .toLowerCase();

            if (mapAccessorKeys.containsKey(methodName))
            {
                mapAccessor.put(methodName, method);
                continue;
            }

            switch (method.getParameterTypes().length)
            {
            case 0:
                getter.put(methodName, new AccessorGetter()
                {

                    @Override
                    public Object get(final Object target,
                            final String key)
                    {
                        try
                        {
                            return method.invoke(target);
                        }
                        catch (final Exception e)
                        {
                            throw new Error(e);
                        }
                    }

                    @Override
                    public Class<?> resultType()
                    {
                        return method.getReturnType();
                    }

                });
                break;

            case 1:
                setter.put(methodName, new AccessorSetter()
                {

                    @Override
                    public Class<?> argumentType()
                    {
                        return method.getParameterTypes()[0];
                    }

                    @Override
                    public void set(final Object target,
                            final Object value)
                    {
                        try
                        {
                            method.invoke(target, value);
                        }
                        catch (final Exception e)
                        {
                            throw new PojoAccessorSetError(target, methodName,
                                    value);
                        }
                    }

                });
                break;
            }
        }
        for (final Field field : targetClass.getFields())
        {
            if (!field.isAccessible())
            {
                try
                {
                    field.setAccessible(true);
                }
                catch (final SecurityException e)
                {
                    continue;
                }
            }
            final String key = field.getName()
                .toLowerCase();
            fields.put(key, new Accessor()
            {

                @Override
                public Class<?> argumentType()
                {
                    return field.getType();
                }

                @Override
                public Object get(final Object target,
                        final String key)
                {
                    try
                    {
                        return field.get(target);
                    }
                    catch (final Exception e)
                    {
                        throw new Error(e);
                    }
                }

                @Override
                public Class<?> resultType()
                {
                    try
                    {
                        return field.getType();
                    }
                    catch (final Exception e)
                    {
                        throw new Error(e);
                    }
                }

                @Override
                public void set(final Object target,
                        final Object value)
                {
                    try
                    {
                        field.set(target, value);
                    }
                    catch (final Exception e)
                    {
                        throw new PojoAccessorSetError(target, key, value);
                    }
                }
            });
        }
    }

    /**
     * フィールドの一覧
     */
    private final HashMap<String, Accessor> fields = new HashMap<String, Accessor>();

    /**
     * getter
     */
    private final HashMap<String, AccessorGetter> getter = new HashMap<String, AccessorGetter>();

    /**
     * map-accessor
     */
    private final HashMap<String, Method> mapAccessor = new HashMap<String, Method>();

    /**
     * setter
     */
    private final HashMap<String, AccessorSetter> setter = new HashMap<String, AccessorSetter>();

    /**
     * 対象のクラス
     */
    private final Class<T> targetClass;
}